////////////////////////////////////////////////////////////////////////////////////////////////
// 
//
// Author: Miajun Yao
// Date: 11/4/2012
// 
////////////////////////////////////////////////////////////////////////////////////////////////



#include "glm/glm.hpp"
#include "glm/gtc/matrix_transform.hpp"

#include <stdio.h>
#include <math.h>
#include <string.h>

#include "lodepng.h"
#include "shaderSetup.h"
#include "GL/glew.h"
#include "GL/glut.h"
#include "GL/gl.h" 

#define M_PI 3.14159

//	define a vertex
typedef struct Vertex
{
  float location[4]; 
  float normal[4]; 
  float color[4]; 
  float texCoord[2];
} Vertex; 

//	define a geometry shape
typedef struct
{
	int nindices;			//	number of indices
	GLuint vboHandle;		//	VBO that contain interleaved positions and colors 
	GLuint indexVBO;		//	index of the VBO
	//	no need to record the vertices and indices after they are passed to GPU VBO
} Geometry;
Geometry sphere, walls[6];	//	6 walls: -x, x, -y, y, -z, z

GLuint cubeTexture, texture[6];

GLuint regularTexMappingObject;		//	regular mapping shader program
GLuint cubeTexMappingObject;		//	cube mapping shader program

glm::mat4 adjust;
glm::mat4 modelview;			//	the input modelView Matrix
glm::mat4 projection;			//	the input projection Matrix

/////////////////////////////////
// glut mouse control 
// 
int xform_mode = 0; 
#define XFORM_NONE    0 
#define XFORM_ROTATE  1
#define XFORM_SCALE 2 

int press_x, press_y; 
int release_x, release_y; 
float x_angle = 0.0; 
float y_angle = 0.0; 
float scale_size = 1; 


//	return the length of a vector
float lengthVector(float* vector, int dimension)
{
	float length = 0;
	for (int i = 0; i < dimension; i++)
		length += vector[i] * vector[i];

	return sqrt(length);
}


//	create a sphere mesh and create a VBO based on that
void createSphere(float radius, int slices, int stacks, float r, float g, float b) 
{
	int nvertices = slices * (stacks + 1);					//	number of sample vertices
	Vertex* vertex = new Vertex[nvertices];					

	float angleUnitSlice = 2 * M_PI/(float)(slices - 1);	//	angle step between two slices
	float angleUnitStack = M_PI/(float)stacks;				//	angle step between two stacks

	//	set up every vertex
	for (int j = 0; j <= stacks; j++)
	{
		float angleStack = (-M_PI / 2) + angleUnitStack * j; 
		for (int i = 0; i < slices; i++) 
		{
			float angleSlice = angleUnitSlice * i; 

			float z = radius * sin(angleStack);
			float xy = radius * cos(angleStack);
			float x = xy * cos(angleSlice);
			float y = xy * sin(angleSlice);
			
			//	GL_NORMALIZE is enabled so no need to nomalize the normal vector
			
			int count = j * slices + i;
			vertex[count].location[0] = x; 
			vertex[count].location[1] = y;
			vertex[count].location[2] = z;
			vertex[count].location[3] = 1.0;
			
			vertex[count].normal[0] = x; 
			vertex[count].normal[1] = y; 
			vertex[count].normal[2] = z; 
			vertex[count].normal[3] = 0.0;

			vertex[count].color[0] = r; 
			vertex[count].color[1] = g; 
			vertex[count].color[2] = b; 
			vertex[count].color[3] = 1.0;

			vertex[count].texCoord[0] = 0; 
			vertex[count].texCoord[1] = 0;	//	this tex coordinate will not be used
		}
	}

	// set up the index array 
	sphere.nindices = stacks * 2 * (slices+1); 
	GLuint *indices = new GLuint[sphere.nindices]; 
	int n = 0; 
	for (int j = 0; j < stacks; j++)
		for (int i = 0; i <= slices; i++) 
		{
			int mi = i % slices;  
			int idx = j * slices + mi;						// mesh[j][mi] 
			int idx2 = (j+1) * slices + mi; 
			indices[n++] = idx; 
			indices[n++] = idx2; 
		}


	//	create VBO based on the vertices and indices
	glGenBuffers(1, &sphere.vboHandle);						// create an interleaved VBO object
	glBindBuffer(GL_ARRAY_BUFFER, sphere.vboHandle);		// bind the first handle 
	glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex) * nvertices, vertex, GL_STATIC_DRAW); // allocate space and copy the position data over
	glBindBuffer(GL_ARRAY_BUFFER, 0);   // clean up 

	glGenBuffers(1, &sphere.indexVBO); 
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, sphere.indexVBO); 
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLuint) * sphere.nindices, indices, GL_STATIC_DRAW);  // load the index data 
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0);				// clean up 

	delete[] vertex;
	delete[] indices;
}

void createTexture()
{
	//	0th part: set up grey texture
	glEnable(GL_TEXTURE_2D);

	//	1st part: set up texture for 6 faces
	std::vector<unsigned char> image[6];
	unsigned width[6], height[6];

	for (int i = 0; i < 6; i++)
	{
		char* fileName;
		switch(i)
		{
		case 0:
			fileName = "cubemap_landscape/negative_x.png";
			break;
		case 1:
			fileName = "cubemap_landscape/positive_x.png";
			break;
		case 2:
			fileName = "cubemap_landscape/negative_y.png";
			break;
		case 3:
			fileName = "cubemap_landscape/positive_y.png";
			break;
		case 4:
			fileName = "cubemap_landscape/negative_z.png";
			break;
		case 5:
			fileName = "cubemap_landscape/positive_z.png";
			break;
		}
		
		unsigned error = lodepng::decode(image[i], width[i], height[i], fileName);
		if(error != 0)
			printf("Error reading png files!\n");

		glGenTextures(1, &texture[i]);
		glBindTexture(GL_TEXTURE_2D, texture[i]);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
		glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width[i], height[i], 0, GL_RGBA, GL_UNSIGNED_BYTE, &image[i][0]);
	}
	glBindTexture(GL_TEXTURE_2D, 0);


	//	2nd part: set up cube texture
	glEnable(GL_TEXTURE_CUBE_MAP);
	glGenTextures(1, &cubeTexture);
	glBindTexture(GL_TEXTURE_CUBE_MAP, cubeTexture);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER,GL_LINEAR);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER,GL_LINEAR);
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
	glTexEnvf(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE, GL_REPLACE);
	
	glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, GL_RGBA, width[0], height[0],0, GL_RGBA, GL_UNSIGNED_BYTE, &image[0][0]); 
	glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, GL_RGBA, width[1], height[1],0, GL_RGBA, GL_UNSIGNED_BYTE, &image[1][0]); 
	glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, GL_RGBA, width[2], height[2],0, GL_RGBA, GL_UNSIGNED_BYTE, &image[2][0]); 
	glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Y, 0, GL_RGBA, width[3], height[3],0, GL_RGBA, GL_UNSIGNED_BYTE, &image[3][0]); 
	glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, GL_RGBA, width[4], height[4],0, GL_RGBA, GL_UNSIGNED_BYTE, &image[4][0]); 
	glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Z, 0, GL_RGBA, width[5], height[5],0, GL_RGBA, GL_UNSIGNED_BYTE, &image[5][0]); 

	glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
}

//	create a cube mesh and create a VBO based on that
void createWalls(float size, float r, float g, float b) 
{
	//	left wall
	Vertex* vertex = new Vertex[4];
	
	float vertexData[] = 
	{
		//	left wall
		-size, size, -size, 1.0, 1.0, 0.0, 0.0, 0.0,  r, g, b, 1.0, 0.0, 0.0, 
		-size, size, size, 1.0, 1.0, 0.0, 0.0, 0.0,  r, g, b, 1.0, 1.0, 0.0, 
		-size, -size, size, 1.0, 1.0, 0.0, 0.0, 0.0,  r, g, b, 1.0, 1.0, 1.0,
		-size, -size, -size, 1.0, 1.0, 0.0, 0.0, 0.0,  r, g, b, 1.0, 0.0, 1.0,
		// right wall
		size, size, size, 1.0, -1.0, 0.0, 0.0, 0.0,  r, g, b, 1.0, 0.0, 0.0, 
		size, size, -size, 1.0, -1.0, 0.0, 0.0, 0.0,  r, g, b, 1.0, 1.0, 0.0, 
		size, -size, -size, 1.0, -1.0, 0.0, 0.0, 0.0,  r, g, b, 1.0, 1.0, 1.0,
		size, -size, size, 1.0, -1.0, 0.0, 0.0, 0.0,  r, g, b, 1.0, 0.0, 1.0,
		//	bottom wall
		-size, -size, size, 1.0, 0.0, 1.0, 0.0, 0.0,  r, g, b, 1.0, 0.0, 0.0, 
		size, -size, size, 1.0, 0.0, 1.0, 0.0, 0.0,  r, g, b, 1.0, 1.0, 0.0, 
		size, -size, -size, 1.0, 0.0, 1.0, 0.0, 0.0,  r, g, b, 1.0, 1.0, 1.0,
		-size, -size, -size, 1.0, 0.0, 1.0, 0.0, 0.0,  r, g, b, 1.0, 0.0, 1.0,
		// top wall
		-size, size, -size, 1.0, 0.0, -1.0, 0.0, 0.0,  r, g, b, 1.0, 0.0, 0.0, 
		size, size, -size, 1.0, 0.0, -1.0, 0.0, 0.0,  r, g, b, 1.0, 1.0, 0.0, 
		size, size, size, 1.0, 0.0, -1.0, 0.0, 0.0,  r, g, b, 1.0, 1.0, 1.0,
		-size, size, size, 1.0, 0.0, -1.0, 0.0, 0.0,  r, g, b, 1.0, 0.0, 1.0,
		//	far wall
		size, size, -size, 1.0, 0.0, 0.0, 1.0, 0.0,  r, g, b, 1.0, 0.0, 0.0, 
		-size, size, -size, 1.0, 0.0, 0.0, 1.0, 0.0,  r, g, b, 1.0, 1.0, 0.0, 
		-size, -size, -size, 1.0, 0.0, 0.0, 1.0, 0.0,  r, g, b, 1.0, 1.0, 1.0,
		size, -size, -size, 1.0, 0.0, 0.0, 1.0, 0.0,  r, g, b, 1.0, 0.0, 1.0,
		// near wall
		-size, size, size, 1.0, 0.0, 0.0, -1.0, 0.0,  r, g, b, 1.0, 0.0, 0.0, 
		size, size, size, 1.0, 0.0, 0.0, -1.0, 0.0,  r, g, b, 1.0, 1.0, 0.0, 
		size, -size, size, 1.0, 0.0, 0.0, -1.0, 0.0,  r, g, b, 1.0, 1.0, 1.0,
		-size, -size, size, 1.0, 0.0, 0.0, -1.0, 0.0,  r, g, b, 1.0, 0.0, 1.0,
	};

	for (int j = 0; j < 6; j++)
	{
		int offsetFace = j * 14 * 4;
		for (int i = 0; i < 4; i++)
		{
			int rowSize = 14;
			int offset = offsetFace + i * rowSize;
			vertex[i].location[0] = vertexData[offset];vertex[i].location[1] = vertexData[offset+1];vertex[i].location[2] = vertexData[offset+2];vertex[i].location[3] = vertexData[offset+3];offset+=4;
			vertex[i].normal[0] = vertexData[offset];vertex[i].normal[1] = vertexData[offset+1];vertex[i].normal[2] = vertexData[offset+2];vertex[i].normal[3] = vertexData[offset+3];offset+=4;
			vertex[i].color[0] = vertexData[offset];vertex[i].color[1] = vertexData[offset+1];vertex[i].color[2] = vertexData[offset+2];vertex[i].color[3] = vertexData[offset+3];offset+=4;
			vertex[i].texCoord[0] = vertexData[offset];vertex[i].texCoord[1] = vertexData[offset+1];
		}
		//	set up indices
		int index[4];
		for (int i = 0; i < 4; i++)
			index[i] = i;

		walls[j].nindices = 4;	
		//	create VBO based on the vertices and indices
		glGenBuffers(1, &walls[j].vboHandle);												// create an interleaved VBO object
		glBindBuffer(GL_ARRAY_BUFFER, walls[j].vboHandle);									// bind the first handle 
		glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex) * 4, vertex, GL_STATIC_DRAW);			// allocate space and copy the position data over

		glGenBuffers(1, &walls[j].indexVBO); 
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, walls[j].indexVBO); 
		glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(index), index, GL_STATIC_DRAW);  // load the index data 
	}
	glBindBuffer(GL_ARRAY_BUFFER, 0);   // clean up 
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);   // clean up 
}


//	bind the sphere mesh buffer and draw a ball
void drawBall()
{
	glUseProgram(cubeTexMappingObject);
	
	GLuint m0 = glGetUniformLocation(cubeTexMappingObject, "projectionMatrix"); 
	GLuint m1 = glGetUniformLocation(cubeTexMappingObject, "modelViewMatrix");	
	GLuint m2 = glGetUniformLocation(cubeTexMappingObject, "normalMatrix");	
	GLuint m3 = glGetUniformLocation(cubeTexMappingObject, "adjustMatrix");	
	glUniformMatrix4fv(m0, 1, GL_FALSE, &projection[0][0]); 
	glUniformMatrix4fv(m1, 1, GL_FALSE, &modelview[0][0]); 
	glm::mat4 normalMatrix = glm::transpose(glm::inverse(modelview));
	glUniformMatrix4fv(m2, 1, GL_FALSE, &normalMatrix[0][0]);
	glUniformMatrix4fv(m3, 1, GL_FALSE, &adjust[0][0]);


	GLint c0 = glGetAttribLocation(cubeTexMappingObject, "position"); 
	GLint c1 = glGetAttribLocation(cubeTexMappingObject, "normal"); 
	GLint c2 = glGetAttribLocation(cubeTexMappingObject, "color"); 
	glEnableVertexAttribArray(c0); 
	glEnableVertexAttribArray(c1); 
	glEnableVertexAttribArray(c2);

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_CUBE_MAP, cubeTexture);

	int uniloc = glGetUniformLocation(cubeTexMappingObject, "texture");
	if (uniloc >= 0) 
		glUniform1i(uniloc, 0); // associate Tex1 to texture unit 0

	glBindBuffer(GL_ARRAY_BUFFER, sphere.vboHandle); 
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, sphere.indexVBO); 
	glVertexAttribPointer(c0,4,GL_FLOAT, GL_FALSE, sizeof(Vertex),(void*)offsetof(Vertex, location[0])); 
	glVertexAttribPointer(c1,4,GL_FLOAT, GL_FALSE, sizeof(Vertex),(void*)offsetof(Vertex, normal[0])); 
	glVertexAttribPointer(c2,4,GL_FLOAT, GL_FALSE, sizeof(Vertex),(void*)offsetof(Vertex, color[0])); 
	glDrawElements(GL_TRIANGLE_STRIP, sphere.nindices, GL_UNSIGNED_INT, (char*) NULL+0); 
}

//	draw a floor using cube
void drawWalls()
{
	glUseProgram(regularTexMappingObject);
	
	GLuint m0 = glGetUniformLocation(regularTexMappingObject, "projectionMatrix"); 
	GLuint m1 = glGetUniformLocation(regularTexMappingObject, "modelViewMatrix");	
	glUniformMatrix4fv(m0, 1, GL_FALSE, &projection[0][0]); 
	glUniformMatrix4fv(m1, 1, GL_FALSE, &modelview[0][0]); 

	GLint c0 = glGetAttribLocation(regularTexMappingObject, "position"); 
	GLint c1 = glGetAttribLocation(regularTexMappingObject, "normal"); 
	GLint c2 = glGetAttribLocation(regularTexMappingObject, "color"); 
	GLint c3 = glGetAttribLocation(regularTexMappingObject, "texCoord");
	glEnableVertexAttribArray(c0); 
	glEnableVertexAttribArray(c1); 
	glEnableVertexAttribArray(c2);
	glEnableVertexAttribArray(c3);

	
	for (int i = 0; i < 6; i++)
	{
		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, texture[i]);

		int uniloc = glGetUniformLocation(regularTexMappingObject, "texture");
		if (uniloc >= 0) 
			glUniform1i(uniloc, 0); // associate Tex1 to texture unit 0

		glBindBuffer(GL_ARRAY_BUFFER, walls[i].vboHandle); 
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, walls[i].indexVBO); 
		glVertexAttribPointer(c0,4,GL_FLOAT, GL_FALSE, sizeof(Vertex),(void*)offsetof(Vertex, location[0])); 
		glVertexAttribPointer(c1,4,GL_FLOAT, GL_FALSE, sizeof(Vertex),(void*)offsetof(Vertex, normal[0])); 
		glVertexAttribPointer(c2,4,GL_FLOAT, GL_FALSE, sizeof(Vertex),(void*)offsetof(Vertex, color[0])); 
		glVertexAttribPointer(c3,2,GL_FLOAT, GL_FALSE, sizeof(Vertex),(void*)offsetof(Vertex, texCoord[0])); 
		glDrawElements(GL_QUADS, walls[i].nindices, GL_UNSIGNED_INT, (char*) NULL+0); 
	}
}


void display() 
{ 
	glEnable(GL_DEPTH_TEST);    //	need depth test to correctly draw 3D objects 
	glClearColor(0,0,0,1);		
	glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); 

	//	set up projection parameters
	projection = glm::perspective(90.0f,1.0f,.1f,100.0f); 
	
	//	set up view parameters
	glm::mat4 view = glm::lookAt(glm::vec3(0, 0, 5.0), 
			       glm::vec3(0.0, 0.0, 0.0), 
			       glm::vec3(0.0, 1.0, 0.0)); 

	//	glut transformation parameters
	glm::mat4 model = glm::mat4(1.0f); 

	model = glm::rotate(model, x_angle, glm::vec3(0.0f, 1.0f, 0.0f)); 
	model = glm::rotate(model, y_angle, glm::vec3(1.0f, 0.0f, 0.0f)); 
	adjust = glm::inverse(model);
	model = glm::scale(model,  glm::vec3(scale_size, scale_size, scale_size)); 

	// construct the modelview and modelview projection matrices 
	modelview = view * model; 

	//	draw what you want	
	drawWalls();
	drawBall();
	

	glutSwapBuffers(); 
} 

//////////////////////////////////////////////////////////////////////////////////
//
//    GLUT stuff 
//
void mymouse(int button, int state, int x, int y)
{
	if (state == GLUT_DOWN) 
	{
		press_x = x; press_y = y; 
		if (button == GLUT_LEFT_BUTTON)
			xform_mode = XFORM_ROTATE; 
		else if (button == GLUT_RIGHT_BUTTON) 
			xform_mode = XFORM_SCALE;   
	}
	else if (state == GLUT_UP) 
	{
		xform_mode = XFORM_NONE; 
	}
}
void mymotion(int x, int y)
{
	if (xform_mode==XFORM_ROTATE) 
	{
		x_angle += (x - press_x)/5.0; 
		if (x_angle > 180) x_angle -= 360; 
		else if (x_angle <-180) x_angle += 360; 
		press_x = x; 
	   
		y_angle += (y - press_y)/5.0; 
		if (y_angle > 180) y_angle -= 360; 
		else if (y_angle <-180) y_angle += 360; 
		press_y = y; 
     }
	else if (xform_mode == XFORM_SCALE)
	{
		float old_size = scale_size;
		scale_size *= (1+ (y - press_y)/60.0); 
		if (scale_size <0) scale_size = old_size; 
		press_y = y; 
    }
    glutPostRedisplay(); 
}

///////////////////////////////////////////////////////////////////////////////
//
int main(int argc, char** argv) 
{ 
	glutInit(&argc, argv); 
	glutInitDisplayMode(GLUT_RGB|GLUT_DOUBLE|GLUT_DEPTH); 
	glutInitWindowSize(600,600); 

	glutCreateWindow("shader cube"); 
	glutDisplayFunc(display); 
	glutMouseFunc(mymouse); 
	glutMotionFunc(mymotion);

	//	initialize GLEW 
	GLenum err = glewInit(); 
	if ( err != GLEW_OK)  
	{
		printf("Error initializing GLEW! \n"); 
		exit(1);
	}
	
	//	create shaders
	regularTexMappingObject = SetupGLSL("regularTexMapping");
	cubeTexMappingObject = SetupGLSL("cubeTexMapping");

	
	//	create geometry VBOs
	createWalls(5.0, 0.8, 0.5, 0.8);
	createTexture();
	createSphere(1.0, 50, 50, 0.5, 0.8, 0.5);

	glutMainLoop(); 
} 
